#!/usr/bin/env python
# -*- coding: utf-8 -*-

__all__ = ['LocalsProcessor', 'CSSContextProcessor', 'NestedProcessor']
__version__ = '$Id$'

from jsCSS.compiler import Compiler
from jsCSS.dom import Rule, Script

class Processor(object):
    """Base class for all processors"""

    def __init__(self):
        self.reset()

    def _restart(self):
        self._offset = self._offset_default

    def _get_offset(self, offset):
        insert_offset = offset
        for position in self._offset:
            if (offset >= position):
                insert_offset += self._offset[position]
        return insert_offset

    def _insert_string(self, target, offset, source):
        insert_offset = self._get_offset(offset)
        result = target[:insert_offset] + source + target[insert_offset:]

        if offset not in self._offset:
            self._offset[offset] = 0
        self._offset[offset] += len(source)

        return result

    def inherit(self, parent):
        self._offset_default = parent._offset
        self._restart()
        return self

    def reset(self):
        self._offset = {}
        self._offset_default = {}
        return self

    def pre_process(self, document, string):
        return string

    def post_process(self, document, string):
        return string

    def _get_first_child(self, node, exclude=None):
        children = node.get_children()
        if len(children) < 1:
            return node
        if exclude is None:
            return children[0]
        child, children = children[0], children[1:]
        while type(child) in exclude and len(children):
            child, children = children[0], children[1:]
        if child.has_children():
            return self._get_first_child(child, exclude)
        return child

    def _get_last_child(self, node):
        children = node.get_children()
        if len(children) < 1:
            return node
        last_child = children[-1]
        if last_child.has_children():
            return self._get_last_child(last_child)
        return last_child

    def __str__(self):
        return '<jsCSS.processor.%s>' % (self.__class__.__name__, )


class CSSContextProcessor(Processor):
    """Create mapping between each CSS property and the JavaScript object
    jsCSS.context"""

    def __init__(self):
        self._compiler = Compiler()

        Processor.__init__(self)

    def pre_process(self, document, string):
        self._restart()
        string = self._insert_string(string, 0, '''<?
if (typeof (jsCSS) === "undefined") jsCSS = {};
jsCSS._contextStack = [];
jsCSS.context = {};
?>
''')
        return self._process_node(document, string)

    def _process_node(self, node, string):
        children = node.get_children()
        for child in children:
            if isinstance(child, Rule):
                string = self._process_rule(child, string)
            if child.has_children():
                string = self._process_node(child, string)
        return string

    def _process_rule(self, rule, string):
        context_code = []
        context_code.append('<?')
        context_code.append('jsCSS._contextStack.push(jsCSS.context);')
        properties = rule.get_properties()
        for property in properties:
            last_property = properties[property][-1]
            last_value = last_property.get_values()[-1]
            context_code.append('jsCSS.context[' + self._get_property_name(property) + '] = ' + self._get_property_value(last_value.value) + ';')
        context_code.append('?>');
        first_child = self._get_first_child(rule, exclude=(Script, ))
        string = self._insert_string(string, first_child.get_position()['start'], '\n'.join(context_code));
        last_child = self._get_last_child(rule)
        string = self._insert_string(string, last_child.get_position()['end'], '<? jsCSS.context = jsCSS._contextStack.pop() ?>');
        return string

    def _get_property_name(self, property):
        regex = r'.replace(/^\s+|\s+$/g, "").replace(/-\D/g, function(match) { return match.charAt(1).toUpperCase(); })'
        return self._compiler.compile_string(property) + regex

    def _get_property_value(self, value):
        return self._compiler.compile_string(value)


class LocalsProcessor(Processor):
    """Create JavaScript scope for each CSS rule"""

    def pre_process(self, document, string):
        self._restart()
        string = self._insert_string(string, 0, '''<?
if (typeof (jsCSS) === "undefined") jsCSS = {};
jsCSS._locals = {
    _stack: [],
    _getProperties: function(obj) {
        var properties = [];
        for (var p in obj) properties.push(p);
        return properties;
    },
    push: function(obj) {
        jsCSS._locals._stack.push(jsCSS._locals._getProperties(obj))
    },
    pop: function(obj) {
        var objProperties = jsCSS._locals._getProperties(obj),
            storedProperties = jsCSS._locals._stack.pop();
        storedProperties.indexOf = function(property) {
            for (var i = 0; i < this.length; i ++)
                if (this[i] === property)
                    return i;
            return -1;
        };
        for (var i = 0; i < objProperties.length; i ++)
            if (storedProperties.indexOf(objProperties[i]) < 0)
                delete obj[objProperties[i]];
    }
};
?>
''')
        return self._process_node(document, string)

    def _process_node(self, node, string):
        children = node.get_children()
        for child in children:
            if isinstance(child, Rule):
                string = self._process_rule(child, string)
            if child.has_children():
                string = self._process_node(child, string)
        return string

    def _process_rule(self, rule, string):
        string = self._insert_string(string, rule.get_position()['end'], '<? jsCSS._locals.push(this) ?>');
        last_child = self._get_last_child(rule)
        string = self._insert_string(string, last_child.get_position()['end'], '<? jsCSS._locals.pop(this) ?>');
        return string


class NestedProcessor(Processor):
    """Expand nested CSS rules"""

    def __init__(self):
        self._string_copy = None

        Processor.__init__(self)

    def post_process(self, document, string):
        self._restart()
        self._string_copy = string
        return self._process_node(document, string)

    def _process_node(self, node, string, path=[]):
        children = node.get_children()
        for child in children:
            if isinstance(child, Rule):
                string = self._process_rule(child, string, path)
            if child.has_children():
                path.append(child)
                string = self._process_node(child, string, path)
                path.pop()
        return string

    def _process_rule(self, rule, string, path):
        # Process only if we are at least one level down
        if len(path) > 0:
            # Build list of parent rules
            rule_path = []
            for child in path:
                if isinstance(child, Rule):
                    rule_path.append(child.value.strip())

            # Find where line starts in the unmodified string
            end_range = range(rule.get_position()['end'] - 1, 0, -1)
            end_line = ''
            for i in end_range:
                if self._string_copy[i] == '\n':
                    break
                end_line += self._string_copy[i]
            # Remove whitespace in front of line (keep indentation level)
            string = self._insert_string(string, rule.get_position()['end'] - len(end_line.rstrip()), ' '.join(rule_path) + ' ');
            # XXX Close only once
            # string = self._insert_string(string, rule.get_position()['end'] - len(end_line), '}\n\n');
        return string
